
Intro
A lite request lib based on fetch with plugins support.
Features:
- 🔥 Use fetch
- 🫡 Similar axios API:
axios.create
/ axios.interceptors
/ .get/post/put/patch/delete/head/options
- 🤙 Support timeout and cancel requests
- 🥷 Plugin support: error retry, cache, throttling, and easily create custom plugins
- 🚀 Lightweight (~6KB, Gzip ~2.6KB)
- 👊 Unit tested and strongly typed 💪
Table of Contents
Why Choose xior?
xior use the native fetch API, offering several advantages:
- Web Standard: Fetch is a widely supported web standard, ensuring compatibility across different environments.
- Built-in Availability: Both Node.js and browsers have built-in fetch implementations, eliminating the need for external dependencies.
- Edge Compatibility: Unlike Axios, xior works seamlessly in edge runtimes, making it suitable for serverless functions and Next.js middleware.
- Convenient API and Plugin Support: xior provides a familiar API similar to Axios, while also offering plugin support for customization and extending functionalities.
Why not just use axios
?
If you want proxy support, use axios
, because xior
doesn't support proxy feature yet
While popular and convenient, Axios currently lacks native edge runtime support (see: https://github.com/axios/axios/issues/5523). This can be an issue for specific use cases like Next.js serverless functions and middleware files, where fetch offers built-in caching and revalidation mechanisms (see: https://nextjs.org/docs/app/api-reference/functions/fetch).
Why choose xior over Custom Fetch Wrappers?
While you can certainly create your own wrapper library around fetch, xior offers a pre-built solution with a familiar API, plugin support for extensibility, and potentially a more streamlined development experience.
Getting Started
Installing
npm install xior
pnpm add xior
bun add xior
yarn add xior
Create instance
import xior from 'xior';
export const xiorInstance = xior.create({
baseURL: 'https://apiexampledomian.com/api',
headers: {
},
});
GET / POST / DELETE / PUT / PATCH / OPTIONS / HEAD
GET
HEAD
method is same usage with GET
async function run() {
const { data } = await xiorInstance.get('/');
const { data: data2 } = await xiorInstance.get('/', { params: { a: 1, b: 2 } });
const { data: data3 } = await xiorInstance.get('/', {
params: { a: 1, b: 2 },
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
});
const { data: data4 } = await xiorInstance.get<{ field1: string; field2: number }>('/');
}
POST
DELETE
/PUT
/PATCH
/OPTIONS
methods are same usage with POST
async function run() {
const { data: data3 } = await xiorInstance.post<{ field1: string; field2: number }>(
'/',
{ a: 1, b: '2' },
{
params: { id: 1 },
headers: {
'content-type': 'application/json',
},
}
);
}
Upload file
xior supports file uploads using the FormData
API and provides an optional 'xior/plugins/progress'
plugin for simulating upload progress, usage similar to Axios.
import Xior from 'xior';
import uploadDownloadProgressPlugin from 'xior/plugins/progress';
const http = Xior.create({});
http.plugins.use(
uploadDownloadProgressPlugin({
progressDuration: 5 * 1000,
})
);
const formData = FormData();
formData.append('file', fileObject);
formData.append('field1', 'val1');
formData.append('field2', 'val2');
http.post('/upload', formData, {
onUploadProgress(e) {
console.log(`Upload progress: ${e.progress}%`);
},
});
Using interceptors
xior supports interceptors similar to Axios, allowing you to modify requests and handle responses programmatically.
Request inteceptors:
import xior, { merge } from 'xior';
const http = xior.create({
});
http.inteceptors.request.use((config) => {
const token = localStorage.getItem('REQUEST_TOKEN');
if (!token) return config;
return merge(config, {
headers: {
Authorization: `Bearer ${token}`,
},
});
});
http.inteceptors.request.use((config) => {
return config;
});
Response inteceptors:
import xior, { merge } from 'xior';
const http = xior.create({});
http.inteceptors.response.use(
(result) => {
const { data, request: config, response: originalResponse } = result;
return result;
},
async (error) => {
if (error?.response?.status === 401) {
localStorage.removeItem('REQUEST_TOKEN');
}
}
);
Timeout and Cancel request
Timeout:
import xior from 'xior';
const instance = xior.create({
timeout: 120 * 1000,
});
await instance.post(
'http://httpbin.org',
{
a: 1,
b: 2,
},
{
timeout: 60 * 1000,
}
);
Cancel request:
import xior from 'xior';
const instance = xior.create();
const controller = new AbortController();
xiorInstance.get('http://httpbin.org', { signal: controller.signal }).then((res) => {
console.log(res.data);
});
class CancelRequestError extends Error {}
controller.abort(new CancelRequestError());
Plugins
xior offers a variety of built-in plugins to enhance its functionality:
Usage:
import xior from 'xior';
import errorRetryPlugin from 'xior/plugins/error-retry';
import throttlePlugin from 'xior/plugins/throttle';
import cachePlugin from 'xior/plugins/cache';
import uploadDownloadProgressPlugin from 'xior/plugins/progress';
const http = xior.create();
http.plugins.use(errorRetryPlugin());
http.plugins.use(throttlePlugin());
http.plugins.use(cachePlugin());
http.plugins.use(uploadDownloadProgressPlugin());
Error retry plugin
Retry the failed request with special times
API:
function errorRetryPlugin(options: {
retryTimes?: number;
retryInterval?: number;
enableRetry?: boolean | (error: Xiorconfig, error: XiorRequestConfig) => boolean;
}): XiorPlugin;
The options
object:
Param | Type | Default value | Description |
---|
retryTimes | number | 2 | Set the retry times for failed request |
retryInterval | number | 3000 | After first time retry, the next retries interval time, default interval is 3 seconds |
enableRetry | boolean | ((config: Xiorconfig, error: XiorRequestConfig) => boolean) | (config, error) => config.method === 'GET' || config.isGet | Default only retry if GET request error and retryTimes > 0 |
Basic usage:
import xior from 'xior';
import errorRetryPlugin from 'xior/plugins/error-retry';
const http = xior.create();
http.plugins.use(
errorRetryPlugin({
retryTimes: 3,
retryInterval: 3000,
})
);
http.get('/api1');
http.get('/api2', { retryTimes: 0 });
http.post('/api1');
http.post('/api1', null, { retryTimes: 5, enableRetry: true });
Request throttle plugin
Throttle GET requests(or custom) most once per threshold milliseconds, filter repeat requests in certain time.
API:
function throttleRequestPlugin(options: {
/** threshold in milliseconds, default: 1000ms */
threshold?: number;
/**
* check if we need enable throttle, default only `GET` method enable
*/
enableThrottle?: boolean | ((config?: XiorRequestConfig) => boolean);
throttleCache?: ICacheLike<RecordedCache>;
}): XiorPlugin;
The options
object:
You can override default value in each request's own config (Except throttleCache
)
Param | Type | Default value | Description |
---|
threshold | number | 1000 | The number of milliseconds to throttle request invocations to |
enableThrottle | boolean | ((config: Xiorconfig) => boolean) | (config) => config.method === 'GET' || config.isGet | Default only enabled in GET request |
throttleCache | CacheLike | lru(10) | CacheLike instance that will be used for storing throttled requests, use tiny-lru module |
Basic usage:
import xior from 'xior';
import throttlePlugin from 'xior/plugins/throttle';
const http = xior.create();
http.plugins.use(throttlePlugin());
http.get('/');
http.get('/');
http.get('/');
http.post('/');
http.post('/');
http.post('/');
http.post('/', null, {
enableThrottle: true,
});
http.post('/', null, {
enableThrottle: true,
});
http.post('/', null, {
enableThrottle: true,
});
http.post('/get', null, {
isGet: true,
});
http.post('/get', null, {
isGet: true,
});
http.post('/get', null, {
isGet: true,
});
Cache plugin
Makes xior cacheable
Good to Know: Next.js already support cache for fetch in server side. More detail
API:
function cachePlugin(options: {
enableCache?: boolean | ((config?: XiorRequestConfig) => boolean);
defaultCache?: ICacheLike<XiorPromise>;
}): XiorPlugin;
The options
object:
Param | Type | Default value | Description |
---|
enableCache | boolean | ((config: Xiorconfig) => boolean) | (config) => config.method === 'GET' || config.isGet | Default only enabled in GET request |
defaultCache | CacheLike | lru(100, 1000*60*5) | will used for storing requests by default, except you define a custom Cache with your request config, use tiny-lru module |
Basic usage:
import xior from 'xior';
import cachePlugin from 'xior/plugins/cache';
const http = xior.create();
http.plugins.use(cachePlugin());
http.get('/users');
http.get('/users');
http.get('/users', { enableCache: false });
http.post('/users');
http.post('/users', { enableCache: true });
const res = await http.post('/users', { enableCache: true });
if (res.fromCache) {
console.log('data from cache!');
}
Advanced:
import xior from 'xior';
import cachePlugin from 'xior/plugins/cache';
import { lru } from 'tiny-lru';
const http = xior.create({
baseURL: 'https://example-domain.com/api',
headers: { 'Cache-Control': 'no-cache' },
});
http.plugins.use(
cachePlugin({
enableCache: false,
})
);
http.get('/users', { enableCache: true });
http.get('/users', { enableCache: true });
const cacheA = lru(100);
http.get('/users', { enableCache: true, defaultCache: cacheA, forceUpdate: true });
Upload and download progress plugin
Enable upload and download progress like axios, but the progress is simulated,
This means it doesn't represent the actual progress but offers a user experience similar to libraries like axios.
API:
function progressPlugin(options: {
/** default: 5*1000 ms */
progressDuration?: number;
}): XiorPlugin;
The options
object:
Param | Type | Default value | Description |
---|
progressDuration | number | 5000 | The upload or download progress grow to 99% duration |
Basic usage:
import xior from 'xior';
import uploadDownloadProgressPlugin from 'xior/plugins/progress';
const http = xior.create({});
http.plugins.use(uploadDownloadProgressPlugin());
const formData = FormData();
formData.append('file', fileObject);
formData.append('field1', 'val1');
formData.append('field2', 'val2');
http.post('/upload', formData, {
progressDuration: 10 * 1000,
onUploadProgress(e) {
console.log(`Upload progress: ${e.progress}%`);
},
});
Create your own custom plugin
xior let you easily to create custom plugins.
Here are examples:
- Simple Logging plugin:
import xior from 'xior';
const instance = xior.create();
instance.plugins.use(function logPlugin(adapter) {
return async (config) => {
const start = Date.now();
const res = await adapter(config);
console.log('%s %s %s take %sms', config.method, config.url, res.status, Date.now() - start);
return res;
};
});
- Check built-in plugins get more inspiration:
Check src/plugins
Helper functions
xior has built-in helper functions, may useful for you:
import lru from 'tiny-lru';
import {
encodeParams,
merge as deepMerge,
delay as sleep,
buildSortedURL,
isAbsoluteURL,
} from 'xior';
FAQ
xior frequently asked questions.
1. Is xior 100% compatiable with axios
?
No, but xior offers a similar API like axios: axios.create
/ axios.interceptors
/ .get/post/put/patch/delete/head/options
.
2. Can I use xior in projects like Bun, Expo, React Native, Next.js, Vue, or Nuxt.js?
Yes, xior works anywhere where the native fetch
API is supported.
Even if the environment doesn't support fetch
, you can use a fetch
polyfill like for older browsers.
3. How do I handle responses with types like 'stream'
, 'document'
, 'arraybuffer'
, or 'blob'
?
To handle such responses, use the responseType: 'stream'
option in your request:
import xior from 'xior';
const http = xior.create({ baseURL });
const { response } = await http.post<{ file: any; body: Record<string, string> }>(
'/stream/10',
null,
{ responseType: 'stream' }
);
const reader = response.body!.getReader();
let chunk;
for await (chunk of readChunks(reader)) {
console.log(`received chunk of size ${chunk.length}`);
}
5. How do I support older browsers?
You can use a polyfill for the fetch
API. Check the file src/tests/polyfill.test.ts
for a potential example.
6. Why is xior named "xior"?
The original name axior
was unavailable on npm, so when removed the "a": axior.
7. Where can I ask additional questions?
If you have any questions, feel free to create issues.
Migrate from axios
to xior
GET
axios:
import axios from 'axios';
axios
.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
})
.finally(function () {
});
axios
.get('/user', {
params: {
ID: 12345,
},
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
})
.finally(function () {
});
async function getUser() {
try {
const response = await axios.get('/user?ID=12345');
console.log(response);
} catch (error) {
console.error(error);
}
}
xior:
import xior from 'xior';
const axios = xior.create();
axios
.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
})
.finally(function () {
});
axios
.get('/user', {
params: {
ID: 12345,
},
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
})
.finally(function () {
});
async function getUser() {
try {
const response = await axios.get('/user?ID=12345');
console.log(response);
} catch (error) {
console.error(error);
}
}
POST
axios:
import axios from 'axios';
axios
.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone',
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
xior:
import xior from 'xior';
const axios = xior.create();
axios
.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone',
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
Creating an instance
axios:
import axios from 'axios';
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: { 'X-Custom-Header': 'foobar' },
});
xior:
import axios from 'xior';
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: { 'X-Custom-Header': 'foobar' },
});
Download file with responseType: 'stream'
(In Node.JS)
axios:
import axios from 'axios';
axios({
method: 'get',
url: 'https://bit.ly/2mTM3nY',
responseType: 'stream',
}).then(function (response) {
response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'));
});
xior:
import xior from 'xior';
const axios = xior.create();
axios
.get('https://bit.ly/2mTM3nY', {
responseType: 'stream',
})
.then(async function ({ response, config }) {
const buffer = Buffer.from(await response.arrayBuffer());
return writeFile('ada_lovelace.jpg', buffer);
});
Use stream
axios:
import axios from 'axios';
import { Readable } from 'stream';
const http = axios.create();
async function getStream(url: string, params: Record<string, any>) {
const { data } = await http.get(url, {
params,
responseType: 'stream',
});
return data;
}
xior:
import axxios from 'xior';
import { Readable } from 'stream';
const http = axios.create();
async function getStream(url: string, params: Record<string, any>) {
const { response } = await http.get(url, {
params,
responseType: 'stream',
});
const stream = convertResponseToReadable(response);
return stream;
}
function convertResponseToReadable(response: Response): Readable {
const reader = response.body.getReader();
return new Readable({
async read() {
const { done, value } = await reader.read();
if (done) {
this.push(null);
} else {
this.push(Buffer.from(value));
}
},
});
}
Migrate from fetch
to xior
GET
fetch:
async function logMovies() {
const response = await fetch('http://example.com/movies.json?page=1&perPage=10');
const movies = await response.json();
console.log(movies);
}
xior:
import xior from 'xior';
const http = xior.create({
baseURL: 'http://example.com',
});
async function logMovies() {
const { data: movies } = await http.get('/movies.json', {
params: {
page: 1,
perPage: 10,
},
});
console.log(movies);
}
POST
fetch:
async function postData(url = '', data = {}) {
const response = await fetch(url, {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: JSON.stringify(data),
});
return response.json();
}
postData('https://example.com/answer', { answer: 42 }).then((data) => {
console.log(data);
});
xior:
import xior from 'xior';
const http = xior.create({
baseURL: 'http://example.com',
});
http
.post(
'/answer',
{ answer: 42 },
{
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
}
)
.then(({ data }) => {
console.log(data);
});
Abort a fetch
fetch:
const controller = new AbortController();
const signal = controller.signal;
const url = 'video.mp4';
const downloadBtn = document.querySelector('#download');
const abortBtn = document.querySelector('#abort');
downloadBtn.addEventListener('click', async () => {
try {
const response = await fetch(url, { signal });
console.log('Download complete', response);
} catch (error) {
console.error(`Download error: ${error.message}`);
}
});
abortBtn.addEventListener('click', () => {
controller.abort();
console.log('Download aborted');
});
xior:
import xior from 'xior';
const http = xior.create();
const controller = new AbortController();
const signal = controller.signal;
const url = 'video.mp4';
const downloadBtn = document.querySelector('#download');
const abortBtn = document.querySelector('#abort');
downloadBtn.addEventListener('click', async () => {
try {
const response = await http.get(url, { signal });
console.log('Download complete', response);
} catch (error) {
console.error(`Download error: ${error.message}`);
}
});
abortBtn.addEventListener('click', () => {
controller.abort();
console.log('Download aborted');
});
Sending a request with credentials included
fetch:
fetch('https://example.com', {
credentials: 'include',
});
xior:
import xior from 'xior';
const http = xior.create();
http.get('https://example.com', {
credentials: 'include',
});
Uploading a file
fetch:
async function upload(formData) {
try {
const response = await fetch('https://example.com/profile/avatar', {
method: 'PUT',
body: formData,
});
const result = await response.json();
console.log('Success:', result);
} catch (error) {
console.error('Error:', error);
}
}
const formData = new FormData();
const fileField = document.querySelector('input[type="file"]');
formData.append('username', 'abc123');
formData.append('avatar', fileField.files[0]);
upload(formData);
xior:
import xior from 'xior';
const http = xior.create({
baseURL: 'https://example.com',
});
async function upload(formData) {
try {
const { data: result } = await http.put('/profile/avatar', formData);
console.log('Success:', result);
} catch (error) {
console.error('Error:', error);
}
}
const formData = new FormData();
const fileField = document.querySelector('input[type="file"]');
formData.append('username', 'abc123');
formData.append('avatar', fileField.files[0]);
upload(formData);
Processing a text file line by line
fetch:
async function* makeTextFileLineIterator(fileURL) {
const utf8Decoder = new TextDecoder('utf-8');
const response = await fetch(fileURL);
const reader = response.body.getReader();
let { value: chunk, done: readerDone } = await reader.read();
chunk = chunk ? utf8Decoder.decode(chunk) : '';
const newline = /\r?\n/gm;
let startIndex = 0;
let result;
while (true) {
const result = newline.exec(chunk);
if (!result) {
if (readerDone) break;
const remainder = chunk.substr(startIndex);
({ value: chunk, done: readerDone } = await reader.read());
chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : '');
startIndex = newline.lastIndex = 0;
continue;
}
yield chunk.substring(startIndex, result.index);
startIndex = newline.lastIndex;
}
if (startIndex < chunk.length) {
yield chunk.substr(startIndex);
}
}
async function run() {
for await (const line of makeTextFileLineIterator(urlOfFile)) {
processLine(line);
}
}
run();
xior:
Good to Know: add {responseType: 'stream'}
options will tell xior no need process response, and return original response in format {response}
import xior from 'xior';
const http = xior.create();
async function* makeTextFileLineIterator(fileURL) {
const utf8Decoder = new TextDecoder('utf-8');
const { response } = await http.get(fileURL, { responseType: 'stream' });
const reader = response.body.getReader();
let { value: chunk, done: readerDone } = await reader.read();
chunk = chunk ? utf8Decoder.decode(chunk) : '';
const newline = /\r?\n/gm;
let startIndex = 0;
let result;
while (true) {
const result = newline.exec(chunk);
if (!result) {
if (readerDone) break;
const remainder = chunk.substr(startIndex);
({ value: chunk, done: readerDone } = await reader.read());
chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : '');
startIndex = newline.lastIndex = 0;
continue;
}
yield chunk.substring(startIndex, result.index);
startIndex = newline.lastIndex;
}
if (startIndex < chunk.length) {
yield chunk.substr(startIndex);
}
}
async function run() {
for await (const line of makeTextFileLineIterator(urlOfFile)) {
processLine(line);
}
}
run();
Thanks
Without the support of these resources, xior wouldn't be possible: